package com.gitee.huanminabc.utils_tools.class_extend.javassist;

import javassist.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.helper.StringUtil;

import java.util.Arrays;
import java.util.List;


/**
 * 使用staticChangeClass修改类,使用dynamicChangeClass动态加载类
 * 区别: staticChangeClass修改类,需要在类加载之前,否则无法修改,而dynamicChangeClass可以在类加载之后修改,但是需要通过特殊的方式调用
 * 优点:
 * 1. staticChangeClass修改类,可以直接调用,不需要特殊的方式调用,而dynamicChangeClass需要通过特殊的方式调用
 * 2. staticChangeClass修改类,只能修改一次,而dynamicChangeClass可以修改多次
 * 注意:
 * 1. staticChangeClass不是啥类都能修改的,需要测试一下如果不能修改,则需要使用dynamicChangeClass
 * 2. staticChangeClass不能被修改的概率是非常大的,你也不清楚这个类是否被加载过,所以建议使用dynamicChangeClass
 * 3. 但是dynamicChangeClass缺陷是需要特殊的方式调用(反射)
 */
@Slf4j
public class ClassExtend {
    public static ClassPool cPool;
    public static AopLoader cl;

    static {
        try {
            cPool = new ClassPool();
            // 添加系统默认的类路径
            cPool.appendSystemPath();
            //添加当前线程的类路径
            cPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
            //导入需要的包
            cPool.importPackage(ClassExtend.class.getPackage().getName() + ".AopFactory");
            cPool.importPackage(ClassExtend.class.getPackage().getName() + ".ClassAspect");
            cl = new AopLoader(cPool);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * 静态修改class文件,添加切面,,要求此类不能在之前已经加载过
     *
     * @param aspect 需要切入的增强类
     */
    @SneakyThrows
    public static void staticChangeClass(AopInfoBo aspect) {
        //获取该需要代理的class对象
        CtClass ctClass = cPool.get(aspect.getClassPath());
        CtMethod declaredMethod = ctClass.getDeclaredMethod(aspect.getMethodName(), ClassExtend.getMethodTypes(aspect.getArgs()));
        CtClassExtend(ctClass, declaredMethod, aspect);
    }

    public static void staticChangeClassList(List<AopInfoBo> appBoList) {
        for (AopInfoBo classAspectClass : appBoList) {
            staticChangeClass(classAspectClass);
        }
    }

    public static void staticChangeClass(AopInfoBo... appBoList) {
        staticChangeClassList(Arrays.asList(appBoList));
    }

    private static void CtClassExtend(CtClass cClass, CtMethod method, AopInfoBo aspect) {
        try {
            //切面增强
            aopExtend(cPool, cClass, method, aspect);
            //重新加载class文件 ,要求此类不能在之前已经加载过
            cClass.toClass();
        } catch (NotFoundException | InstantiationException | IllegalAccessException | CannotCompileException e) {
            e.printStackTrace();
        }
    }


    //===============================================================================================================================


    /**
     * /动态加载类,获取便捷调用扩展类的工具类
     *
     * @param aspect 需要切入的增强类
     * @param clazz  需要代理的类
     * @param args   代理类的构造函数参数,用于创建代理类的实例,如果是无参构造函数,则不需要传递
     * @param <T>
     * @return 返回一个动态类, 可以调用动态类的方法和属性
     */
    @SneakyThrows
    public static <T> DynamicClass dynamicChangeClass(AopInfoBo aspect, Class<T> clazz, Object... args) {
        cl.addAspect(aspect);
        Class aClass = cl.findClass(clazz.getName());
        return new DynamicClass(aClass, args);
    }

    //查询加载的动态类
    public static Class dynamicLoadClass(Class clazz) {
        return cl.findClass(clazz.getName());
    }


    //添加增强,一个方法可以被多个增强类增强
    public static void aopExtends(ClassPool cPool, CtClass cClass, CtMethod cMethods, List<AopInfoBo> aspects) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException {
        for (AopInfoBo aspect : aspects) {
            aopExtend(cPool, cClass, cMethods, aspect);
        }
    }


    @SneakyThrows
    public static boolean aopExtend(ClassPool cPool, CtClass cClass, CtMethod cMethod, AopInfoBo aspect) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException {

        //判断不是接口,注解,枚举,内部类
        if (cClass.isInterface() || cClass.isAnnotation() || cClass.isEnum() || cClass.getName().contains("$")) {
            log.error("not[接口,注解,枚举,内部类] 该类不是普通类,无法添加切面:{}", cClass.getName());
            throw new RuntimeException("not[接口,注解,枚举,内部类] 该类不是普通类,无法添加切面:" + cClass.getName());
        }

        //判断切面是否存在
        if (AopFactory.getAspect(aspect.getClassPath()) == null) {
            //添加切面
            AopFactory.addAspect(aspect.getAspect());
        }
        //判断类是否冻结,如果冻结了,需要解冻
        if (cClass.isFrozen()) {
            cClass.defrost();
        }

        //修改方法
        if (!StringUtil.isBlank(aspect.getAspect().modifyMethodBody(cPool))) {
            cMethod.setBody(aspect.getAspect().modifyMethodBody(cPool));
        }

        cMethod.insertBefore("ClassAspect classAspect = AopFactory.getAspect(\"" + aspect.getAspect().getClass().getName() + "\");" +
                "if(classAspect!=null){classAspect.before($class,\"" + cMethod.getName() + "\",$args);};");


        cMethod.insertAfter("ClassAspect classAspect = AopFactory.getAspect(\"" + aspect.getAspect().getClass().getName() + "\");" +
                "if(classAspect!=null){classAspect.after($class,\"" + cMethod.getName() + "\",$args,$_);};");


        cMethod.addCatch("{" +
                        "ClassAspect classAspect = AopFactory.getAspect(\"" + aspect.getAspect().getClass().getName() + "\");" +
                        "if(classAspect!=null){classAspect.afterThrowing($class,\"" + cMethod.getName() + "\",$args,$e);};" +
                        "throw $e;" +
                        "}",
                cPool.get("java.lang.Exception"));


        //返回通知,无论是否有异常都会执行
        cMethod.insertAfter("ClassAspect classAspect = AopFactory.getAspect(\"" + aspect.getAspect().getClass().getName() + "\");" +
                "if(classAspect!=null){classAspect.afterReturning($class,\"" + cMethod.getName() + "\",$args,$_);};", true);
        return true;
    }

    //解析出方法类型数组
    public static CtClass[] getMethodTypes(Class[] objects) throws NotFoundException {
        CtClass[] classes = new CtClass[objects.length];
        for (int i2 = 0; i2 < objects.length; i2++) {
            classes[i2] = cPool.get(objects[i2].getName());
        }
        return classes;
    }


}
